The goal of this notebook is to compare pseudobulk and bulk calculations to determine which pseudobulk calculation should we proceed with for modeling: the log2 of the sum of raw counts (pseudobulk_log_counts) or the DESeq2-normalized sum of raw counts (pseudobulk_deseq). To this end, we’ll visualize expression distributions, both on their own and compared to bulk TPM.

Setup

renv::load()

library(ggplot2)
theme_set(theme_bw())

Paths

data_dir <- here::here("analysis", "pseudobulk-bulk-prediction", "data")
tpm_dir <- file.path(data_dir, "tpm")
pseudobulk_dir <- file.path(data_dir, "pseudobulk")


tpm_files <- list.files(
  path = tpm_dir,
  full.names = TRUE,
  pattern = "-tpm\\.tsv$"
)
tpm_names <- stringr::str_split_i(basename(tpm_files), pattern = "-", i = 1)
names(tpm_files) <- tpm_names


pseudobulk_files <- list.files(
  path = pseudobulk_dir,
  full.names = TRUE,
  pattern = "-pseudobulk\\.tsv$"
)
pseudobulk_names <- stringr::str_split_i(basename(pseudobulk_files), pattern = "-", i = 1)
names(pseudobulk_files) <- pseudobulk_names

# Make sure we have the same projects, in the same order
stopifnot(
  all.equal(names(tpm_files), names(pseudobulk_files))
)

Read and prepare input data

We’ll make both a long and wide version of the data for convenience throughout the notebook.

project_long_df_list <- purrr::map2(
  tpm_files, 
  pseudobulk_files, 
  \(tpm_file, pseudo_file) {
    
    dplyr::bind_rows(
      # TPM needs to be in log2 space
      readr::read_tsv(tpm_file, show_col_types = FALSE) |>
        dplyr::mutate(expression = log2(expression)),
      readr::read_tsv(pseudo_file, show_col_types = FALSE)
    )
  }
)

# Make a wide version as well
project_wide_df_list <- project_long_df_list |>
  purrr::map(
    \(df) {
      df |>
        tidyr::pivot_wider(names_from = expression_type, values_from = expression)
    }
)

Distributions of expression values

First, let’s get a general sense of the scale of values in the pseudobulk quantities:

project_wide_df_list |>
  purrr::map(
    \(df) {
      df |>
        # consider only genes with values not lost to transformation
        dplyr::filter(is.finite(pseudobulk_log_counts)) |>
        dplyr::pull(pseudobulk_log_counts) |>
        summary()
    }
  )
$SCPCP000001
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  0.000   2.322   5.322   5.301   8.134  19.707 

$SCPCP000002
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  0.000   2.322   5.322   5.328   8.134  20.643 

$SCPCP000006
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  0.000   2.000   4.700   4.846   7.375  20.125 

$SCPCP000009
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  0.000   2.000   4.700   4.850   7.418  21.389 

$SCPCP000017
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  0.000   2.000   5.285   5.377   8.480  21.760 
project_wide_df_list |>
  purrr::map(
    \(df) summary(df$pseudobulk_deseq)
  )
$SCPCP000001
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
 -2.831   0.000   0.000   2.014   4.627  20.689 

$SCPCP000002
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
 -2.609   0.000   0.000   2.066   4.653  19.238 

$SCPCP000006
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
-2.5231 -0.2927  0.0000  2.0203  4.8461 18.2792 

$SCPCP000009
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
-1.5802  0.0000  0.2317  2.5097  5.2150 19.8548 

$SCPCP000017
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
-2.5783 -0.2888  0.0000  2.2524  5.2874 22.8423 

Due to the different transformation approaches, the pseudobulk_deseq version has some negatives for fractional values, but pseudobulk_log_counts has a lower bound of 0. These different scales of data are important to keep in mind.

Undefined values

Due to the log2 transformation and sparsity of scRNA-seq counts, many values in pseudobulk_log_counts are expected to be -Inf, aka essentially unobserved in the single-cell data. We’ll look at histograms, per project, of the percentage of genes with defined:

finite_df <- project_wide_df_list |>
  purrr::map(
    \(df) {
      df |>
        dplyr::count(sample_id, is_finite = is.finite(pseudobulk_log_counts)) |>
        tidyr::pivot_wider(
          names_from = is_finite, 
          values_from = n, 
          names_prefix = "finite_",
          values_fill = 0
        ) |>
        dplyr::mutate(percent_genes_with_expression = finite_TRUE/(finite_FALSE + finite_TRUE))
    }) |>
  purrr::list_rbind(names_to = "project_id")


ggplot(finite_df) +
  aes(x = percent_genes_with_expression, fill = project_id) + 
  geom_histogram(bins = 15) + 
  facet_wrap(vars(project_id), nrow = 1) + 
  theme(legend.position = "none")

It looks like about half the genes, but ranging from ~20 - 60%, of genes in a given sample are non-zero. This means about 40-80% of genes in the genome are not observed per sample.

What are the pseudobulk_deseq values where the pseudobulk_log_counts values (above) are -Inf?

deseq_inf_df <- project_wide_df_list |>
  purrr::map(
    \(df) dplyr::filter(df, is.infinite(pseudobulk_log_counts))
  )|>
  purrr::list_rbind(names_to = "project_id")


ggplot(deseq_inf_df) +
  aes(x = pseudobulk_deseq, fill = project_id) + 
  geom_histogram(bins = 15) + 
  facet_wrap(vars(project_id), nrow = 1) + 
  theme(legend.position = "none")

So, lots of the 0s are here which we expect to see. Are all the 0s here?

If all the 0s are the same as all the infinites, then there should be no rows where 0 and finite co-occur:

project_wide_df_list |>
  purrr::map(
    \(df) {
      df |>
        # when deseq is 0, is log_counts always -inf?
        dplyr::filter(
          pseudobulk_deseq == 0, 
          is.finite(pseudobulk_log_counts)
        ) |>
        nrow()
  })
$SCPCP000001
[1] 0

$SCPCP000002
[1] 0

$SCPCP000006
[1] 0

$SCPCP000009
[1] 0

$SCPCP000017
[1] 0

Indeed, pseudobulk_log_counts is never finite (aka, it is always -Inf) when pseudobulk_deseq is 0, which is good to know.

Let’s also check if we have the same genes between bulk and single-cell. Any genes present in one modality but not the other will have NA expression (transforming any 0 expressions will have produced -Inf, not NA, so those NAs are from joining).

Are any in single-cell but not bulk?

project_wide_df_list |>
  purrr::map(\(df){
    df |> 
      dplyr::filter(!is.na(bulk_tpm), 
                    is.na(pseudobulk_deseq), 
                    is.na(pseudobulk_log_counts)) 
  }) |>
  dplyr::bind_rows()

Nope! How about single-cell but not bulk?

only_single_cell <- project_wide_df_list |>
  purrr::map(\(df){
    df |> 
      dplyr::filter(is.na(bulk_tpm), 
                    !is.na(pseudobulk_deseq), 
                    !is.na(pseudobulk_log_counts)) 
  }) |>
  purrr::list_rbind(names_to = "project_id") |>
  dplyr::count(project_id, ensembl_id)
only_single_cell

Yup! Plus, these numbers are all the sample numbers…

only_single_cell |>
  dplyr::count(ensembl_id)

..and it’s the same genes all round (probably a reference thing…?).

In any case, what is the expression of these genes in pseudobulk?

genes_not_in_bulk <- project_wide_df_list |>
  purrr::map(
  \(df) df |> dplyr::filter(is.na(bulk_tpm)) 
) |>
  purrr::list_rbind(names_to = "project_id") |>
  dplyr::select(-bulk_tpm) |>
  tidyr::pivot_longer(
    contains("pseudobulk"), 
    names_to = "expression_type", 
    values_to = "expression"
  )

ggplot(genes_not_in_bulk) + 
  aes(x = expression_type, y = expression) +
  geom_boxplot() + 
  facet_wrap(vars(project_id), nrow = 1) +
  theme(axis.text.x = element_text(angle = 30, hjust = 1))
Warning: Removed 4142 rows containing non-finite outside the scale range
(`stat_boxplot()`).

Some of these genes actually have decent values in single-cell.

Full distributions

Here, we visualize distributions of all quantities:

project_long_df_list |>
  purrr::imap(
    \(df, project_id) {
       ggplot(df) + 
          aes(x = expression, fill = expression_type) + 
          geom_density(alpha = 0.5) + 
          scale_fill_brewer(palette = "Dark2") + 
          facet_wrap(vars(expression_type), scales = "free", nrow = 4) +
          ggtitle(project_id) +
          theme(legend.position = "none")
    }
  ) |>
  patchwork::wrap_plots(guides = "collect", nrow = 1)

Relationship between quantities

This section will look at the relationship between bulk and pseudobulk quantities with:

Scatterplots

What does the relationship look like between bulk and each flavor of pseudobulk? Plots are organized by project and:

  • Left-side panels are bulk tpm ~ deseq pseudocounts
  • Right-side panels are bulk tpm ~ log_counts pseudocounts
project_wide_df_list |>
  purrr::imap(
    \(df, project_id) {
      
      p1 <- ggplot(df) + 
        aes(x = pseudobulk_deseq, y = bulk_tpm) + 
        geom_point(alpha = 0.2, size = 0.5) + 
        geom_smooth(method = "lm") + 
        facet_wrap(vars(sample_id), nrow = 5) +
        ggtitle("bulk tpm ~ deseq")
      
      p2 <- ggplot(df) + 
        aes(x = pseudobulk_log_counts, y = bulk_tpm) + 
        geom_point(alpha = 0.2, size = 0.5) + 
        geom_smooth(method = "lm") +
        facet_wrap(vars(sample_id), nrow = 5) +
        ggtitle("bulk tpm ~ logcounts") 
           
      
      patchwork::wrap_plots(p1, p2) 

    }
  ) |>
  patchwork::wrap_plots(nrow = 5)

Statistics

Let’s now get some stats for each sample. We’ll fit a linear model for each sample, and display some quantities below both as boxplots and the full table.

plot_stats <- function(df, column, title) {
  ggplot(df) + 
    aes(x = expression_type, y = {{column}}, color = expression_type) + 
    geom_boxplot() + 
    scale_color_brewer(palette = "Dark2") +
    ggtitle(title) +
    facet_wrap(vars(project_id), nrow = 1) +
    theme(legend.position = "none")
}

model_samples <- function(id, df) {
  sample_df <- df |>
    dplyr::filter(sample_id == id) 
  
  df_deseq <- sample_df |>
    dplyr::filter(is.finite(pseudobulk_deseq), is.finite(bulk_tpm))
  fit_deseq <- lm(bulk_tpm ~ pseudobulk_deseq, data = df_deseq) 

  df_log_counts <- sample_df |>
      dplyr::filter(is.finite(pseudobulk_log_counts), is.finite(bulk_tpm))
  fit_log_counts <- lm(bulk_tpm ~ pseudobulk_log_counts, data = df_log_counts)
      
  # Tabulate and return some fit stats
  data.frame(
    expression_type = c("deseq", "log_counts"),
    rsquared = c(broom::glance(fit_deseq)$r.squared, broom::glance(fit_log_counts)$r.squared), 
    coeff = c(broom::tidy(fit_deseq)$estimate[2], broom::tidy(fit_log_counts)$estimate[2]), 
    residual_sd = c(broom::glance(fit_deseq)$sigma, broom::glance(fit_log_counts)$sigma)
  )
}

stats_df <- project_wide_df_list |>
  purrr::map(
    \(df) {
      
      # We need to map over sample ids now
      samples <- unique(df$sample_id)
      names(samples) <- samples
      
      fit_table <- samples |>
        purrr::map(model_samples, df) |>
        purrr::list_rbind(names_to = "sample_id")
      
      return(fit_table)

    }
  ) |>
  # now, combine all projects into a single table
  purrr::list_rbind(names_to = "project_id")

patchwork::wrap_plots(
  plot_stats(stats_df, rsquared, "rsquared"),
  plot_stats(stats_df, coeff, "coeff"), 
  plot_stats(stats_df, residual_sd, "residual_sd"), 
  nrow = 3
) 

  • Pseudobulk quantities are mostly similar, and pseudobolk_deseq tends to outperform pseudobolk_log_counts for all projects except SCPCP000009, but notably there are only 3 samples for this project
  • Correlations are mostly zero or very low for SCPCP000017, and SCPCP000001 and SCPCP000002 show the strongest relationships overall

All the actual values are here:

stats_df

Session info

sessionInfo()
R version 4.4.0 (2024-04-24)
Platform: aarch64-apple-darwin20
Running under: macOS 15.2

Matrix products: default
BLAS:   /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRblas.0.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.0

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: America/New_York
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] ggplot2_3.5.1

loaded via a namespace (and not attached):
 [1] sass_0.4.9          generics_0.1.3      tidyr_1.3.1        
 [4] renv_1.0.11         stringi_1.8.4       lattice_0.22-6     
 [7] hms_1.1.3           digest_0.6.37       magrittr_2.0.3     
[10] evaluate_1.0.1      grid_4.4.0          RColorBrewer_1.1-3 
[13] fastmap_1.2.0       Matrix_1.7-1        rprojroot_2.0.4    
[16] jsonlite_1.8.9      backports_1.5.0     BiocManager_1.30.25
[19] mgcv_1.9-1          purrr_1.0.2         scales_1.3.0       
[22] jquerylib_0.1.4     cli_3.6.3           rlang_1.1.4        
[25] crayon_1.5.3        bit64_4.5.2         munsell_0.5.1      
[28] splines_4.4.0       withr_3.0.2         cachem_1.1.0       
[31] yaml_2.3.10         tools_4.4.0         parallel_4.4.0     
[34] tzdb_0.4.0          dplyr_1.1.4         colorspace_2.1-1   
[37] here_1.0.1          broom_1.0.7         vctrs_0.6.5        
[40] R6_2.5.1            lifecycle_1.0.4     stringr_1.5.1      
[43] bit_4.5.0.1         vroom_1.6.5         pkgconfig_2.0.3    
[46] pillar_1.10.0       bslib_0.8.0         gtable_0.3.6       
[49] glue_1.8.0          xfun_0.49           tibble_3.2.1       
[52] tidyselect_1.2.1    knitr_1.49          farver_2.1.2       
[55] htmltools_0.5.8.1   nlme_3.1-166        patchwork_1.3.0    
[58] rmarkdown_2.29      labeling_0.4.3      readr_2.1.5        
[61] compiler_4.4.0     
LS0tCnRpdGxlOiAiUHNldWRvYnVsayBhbmQgYnVsayBkYXRhIGRpc3RyaWJ1dGlvbnMiCmF1dGhvcjogU3RlcGhhbmllIEouIFNwaWVsbWFuCmRhdGU6ICJgciBTeXMuRGF0ZSgpYCIKb3V0cHV0OgogIGh0bWxfbm90ZWJvb2s6CiAgICB0b2M6IHRydWUKICAgIHRvY19kZXB0aDogMwogICAgY29kZV9mb2xkaW5nOiBzaG93Ci0tLQoKVGhlIGdvYWwgb2YgdGhpcyBub3RlYm9vayBpcyB0byBjb21wYXJlIHBzZXVkb2J1bGsgYW5kIGJ1bGsgY2FsY3VsYXRpb25zIHRvIGRldGVybWluZSB3aGljaCBwc2V1ZG9idWxrIGNhbGN1bGF0aW9uIHNob3VsZCB3ZSBwcm9jZWVkIHdpdGggZm9yIG1vZGVsaW5nOiB0aGUgbG9nMiBvZiB0aGUgc3VtIG9mIHJhdyBjb3VudHMgKGBwc2V1ZG9idWxrX2xvZ19jb3VudHNgKSBvciB0aGUgYERFU2VxMmAtbm9ybWFsaXplZCBzdW0gb2YgcmF3IGNvdW50cyAoYHBzZXVkb2J1bGtfZGVzZXFgKS4KVG8gdGhpcyBlbmQsIHdlJ2xsIHZpc3VhbGl6ZSBleHByZXNzaW9uIGRpc3RyaWJ1dGlvbnMsIGJvdGggb24gdGhlaXIgb3duIGFuZCBjb21wYXJlZCB0byBidWxrIFRQTS4gCgoKIyMgU2V0dXAKCmBgYHtyIHNldHVwfQpyZW52Ojpsb2FkKCkKCmxpYnJhcnkoZ2dwbG90MikKdGhlbWVfc2V0KHRoZW1lX2J3KCkpCmBgYAoKIyMjIFBhdGhzCgpgYGB7ciBwYXRoc30KZGF0YV9kaXIgPC0gaGVyZTo6aGVyZSgiYW5hbHlzaXMiLCAicHNldWRvYnVsay1idWxrLXByZWRpY3Rpb24iLCAiZGF0YSIpCnRwbV9kaXIgPC0gZmlsZS5wYXRoKGRhdGFfZGlyLCAidHBtIikKcHNldWRvYnVsa19kaXIgPC0gZmlsZS5wYXRoKGRhdGFfZGlyLCAicHNldWRvYnVsayIpCgoKdHBtX2ZpbGVzIDwtIGxpc3QuZmlsZXMoCiAgcGF0aCA9IHRwbV9kaXIsCiAgZnVsbC5uYW1lcyA9IFRSVUUsCiAgcGF0dGVybiA9ICItdHBtXFwudHN2JCIKKQp0cG1fbmFtZXMgPC0gc3RyaW5ncjo6c3RyX3NwbGl0X2koYmFzZW5hbWUodHBtX2ZpbGVzKSwgcGF0dGVybiA9ICItIiwgaSA9IDEpCm5hbWVzKHRwbV9maWxlcykgPC0gdHBtX25hbWVzCgoKcHNldWRvYnVsa19maWxlcyA8LSBsaXN0LmZpbGVzKAogIHBhdGggPSBwc2V1ZG9idWxrX2RpciwKICBmdWxsLm5hbWVzID0gVFJVRSwKICBwYXR0ZXJuID0gIi1wc2V1ZG9idWxrXFwudHN2JCIKKQpwc2V1ZG9idWxrX25hbWVzIDwtIHN0cmluZ3I6OnN0cl9zcGxpdF9pKGJhc2VuYW1lKHBzZXVkb2J1bGtfZmlsZXMpLCBwYXR0ZXJuID0gIi0iLCBpID0gMSkKbmFtZXMocHNldWRvYnVsa19maWxlcykgPC0gcHNldWRvYnVsa19uYW1lcwoKIyBNYWtlIHN1cmUgd2UgaGF2ZSB0aGUgc2FtZSBwcm9qZWN0cywgaW4gdGhlIHNhbWUgb3JkZXIKc3RvcGlmbm90KAogIGFsbC5lcXVhbChuYW1lcyh0cG1fZmlsZXMpLCBuYW1lcyhwc2V1ZG9idWxrX2ZpbGVzKSkKKQpgYGAKCiMjIyBSZWFkIGFuZCBwcmVwYXJlIGlucHV0IGRhdGEKCldlJ2xsIG1ha2UgYm90aCBhIGxvbmcgYW5kIHdpZGUgdmVyc2lvbiBvZiB0aGUgZGF0YSBmb3IgY29udmVuaWVuY2UgdGhyb3VnaG91dCB0aGUgbm90ZWJvb2suCgoKYGBge3J9CnByb2plY3RfbG9uZ19kZl9saXN0IDwtIHB1cnJyOjptYXAyKAogIHRwbV9maWxlcywgCiAgcHNldWRvYnVsa19maWxlcywgCiAgXCh0cG1fZmlsZSwgcHNldWRvX2ZpbGUpIHsKICAgIAogICAgZHBseXI6OmJpbmRfcm93cygKICAgICAgIyBUUE0gbmVlZHMgdG8gYmUgaW4gbG9nMiBzcGFjZQogICAgICByZWFkcjo6cmVhZF90c3YodHBtX2ZpbGUsIHNob3dfY29sX3R5cGVzID0gRkFMU0UpIHw+CiAgICAgICAgZHBseXI6Om11dGF0ZShleHByZXNzaW9uID0gbG9nMihleHByZXNzaW9uKSksCiAgICAgIHJlYWRyOjpyZWFkX3Rzdihwc2V1ZG9fZmlsZSwgc2hvd19jb2xfdHlwZXMgPSBGQUxTRSkKICAgICkKICB9CikKCiMgTWFrZSBhIHdpZGUgdmVyc2lvbiBhcyB3ZWxsCnByb2plY3Rfd2lkZV9kZl9saXN0IDwtIHByb2plY3RfbG9uZ19kZl9saXN0IHw+CiAgcHVycnI6Om1hcCgKICAgIFwoZGYpIHsKICAgICAgZGYgfD4KICAgICAgICB0aWR5cjo6cGl2b3Rfd2lkZXIobmFtZXNfZnJvbSA9IGV4cHJlc3Npb25fdHlwZSwgdmFsdWVzX2Zyb20gPSBleHByZXNzaW9uKQogICAgfQopCmBgYAoKCiMjIERpc3RyaWJ1dGlvbnMgb2YgZXhwcmVzc2lvbiB2YWx1ZXMKCgpGaXJzdCwgbGV0J3MgZ2V0IGEgZ2VuZXJhbCBzZW5zZSBvZiB0aGUgc2NhbGUgb2YgdmFsdWVzIGluIHRoZSBwc2V1ZG9idWxrIHF1YW50aXRpZXM6CgpgYGB7cn0KcHJvamVjdF93aWRlX2RmX2xpc3QgfD4KICBwdXJycjo6bWFwKAogICAgXChkZikgewogICAgICBkZiB8PgogICAgICAgICMgY29uc2lkZXIgb25seSBnZW5lcyB3aXRoIHZhbHVlcyBub3QgbG9zdCB0byB0cmFuc2Zvcm1hdGlvbgogICAgICAgIGRwbHlyOjpmaWx0ZXIoaXMuZmluaXRlKHBzZXVkb2J1bGtfbG9nX2NvdW50cykpIHw+CiAgICAgICAgZHBseXI6OnB1bGwocHNldWRvYnVsa19sb2dfY291bnRzKSB8PgogICAgICAgIHN1bW1hcnkoKQogICAgfQogICkKYGBgCgoKYGBge3J9CnByb2plY3Rfd2lkZV9kZl9saXN0IHw+CiAgcHVycnI6Om1hcCgKICAgIFwoZGYpIHN1bW1hcnkoZGYkcHNldWRvYnVsa19kZXNlcSkKICApCmBgYAoKCgpEdWUgdG8gdGhlIGRpZmZlcmVudCB0cmFuc2Zvcm1hdGlvbiBhcHByb2FjaGVzLCB0aGUgYHBzZXVkb2J1bGtfZGVzZXFgIHZlcnNpb24gaGFzIHNvbWUgbmVnYXRpdmVzIGZvciBmcmFjdGlvbmFsIHZhbHVlcywgYnV0IGBwc2V1ZG9idWxrX2xvZ19jb3VudHNgIGhhcyBhIGxvd2VyIGJvdW5kIG9mIDAuClRoZXNlIGRpZmZlcmVudCBzY2FsZXMgb2YgZGF0YSBhcmUgaW1wb3J0YW50IHRvIGtlZXAgaW4gbWluZC4KCgojIyMgVW5kZWZpbmVkIHZhbHVlcwoKRHVlIHRvIHRoZSBgbG9nMmAgdHJhbnNmb3JtYXRpb24gYW5kIHNwYXJzaXR5IG9mIHNjUk5BLXNlcSBjb3VudHMsIG1hbnkgdmFsdWVzIGluIGBwc2V1ZG9idWxrX2xvZ19jb3VudHNgIGFyZSBleHBlY3RlZCB0byBiZSBgLUluZmAsIGFrYSBlc3NlbnRpYWxseSB1bm9ic2VydmVkIGluIHRoZSBzaW5nbGUtY2VsbCBkYXRhLgpXZSdsbCBsb29rIGF0IGhpc3RvZ3JhbXMsIHBlciBwcm9qZWN0LCBvZiB0aGUgcGVyY2VudGFnZSBvZiBnZW5lcyB3aXRoIF9kZWZpbmVkXzoKCmBgYHtyIGZpZy5oZWlnaHQgPSAzLCBmaWcud2lkdGggPSA4fQpmaW5pdGVfZGYgPC0gcHJvamVjdF93aWRlX2RmX2xpc3QgfD4KICBwdXJycjo6bWFwKAogICAgXChkZikgewogICAgICBkZiB8PgogICAgICAgIGRwbHlyOjpjb3VudChzYW1wbGVfaWQsIGlzX2Zpbml0ZSA9IGlzLmZpbml0ZShwc2V1ZG9idWxrX2xvZ19jb3VudHMpKSB8PgogICAgICAgIHRpZHlyOjpwaXZvdF93aWRlcigKICAgICAgICAgIG5hbWVzX2Zyb20gPSBpc19maW5pdGUsIAogICAgICAgICAgdmFsdWVzX2Zyb20gPSBuLCAKICAgICAgICAgIG5hbWVzX3ByZWZpeCA9ICJmaW5pdGVfIiwKICAgICAgICAgIHZhbHVlc19maWxsID0gMAogICAgICAgICkgfD4KICAgICAgICBkcGx5cjo6bXV0YXRlKHBlcmNlbnRfZ2VuZXNfd2l0aF9leHByZXNzaW9uID0gZmluaXRlX1RSVUUvKGZpbml0ZV9GQUxTRSArIGZpbml0ZV9UUlVFKSkKICAgIH0pIHw+CiAgcHVycnI6Omxpc3RfcmJpbmQobmFtZXNfdG8gPSAicHJvamVjdF9pZCIpCgoKZ2dwbG90KGZpbml0ZV9kZikgKwogIGFlcyh4ID0gcGVyY2VudF9nZW5lc193aXRoX2V4cHJlc3Npb24sIGZpbGwgPSBwcm9qZWN0X2lkKSArIAogIGdlb21faGlzdG9ncmFtKGJpbnMgPSAxNSkgKyAKICBmYWNldF93cmFwKHZhcnMocHJvamVjdF9pZCksIG5yb3cgPSAxKSArIAogIHRoZW1lKGxlZ2VuZC5wb3NpdGlvbiA9ICJub25lIikKYGBgCkl0IGxvb2tzIGxpa2UgYWJvdXQgaGFsZiB0aGUgZ2VuZXMsIGJ1dCByYW5naW5nIGZyb20gfjIwIC0gNjBcJSwgb2YgZ2VuZXMgaW4gYSBnaXZlbiBzYW1wbGUgYXJlIG5vbi16ZXJvLgpUaGlzIG1lYW5zIGFib3V0IDQwLTgwXCUgb2YgZ2VuZXMgaW4gdGhlIGdlbm9tZSBhcmUgbm90IG9ic2VydmVkIHBlciBzYW1wbGUuCgoKV2hhdCBhcmUgdGhlIGBwc2V1ZG9idWxrX2Rlc2VxYCB2YWx1ZXMgd2hlcmUgdGhlIGBwc2V1ZG9idWxrX2xvZ19jb3VudHNgIHZhbHVlcyAoYWJvdmUpIGFyZSBgLUluZmA/CgpgYGB7ciBmaWcuaGVpZ2h0ID0gMywgZmlnLndpZHRoID0gOH0KZGVzZXFfaW5mX2RmIDwtIHByb2plY3Rfd2lkZV9kZl9saXN0IHw+CiAgcHVycnI6Om1hcCgKICAgIFwoZGYpIGRwbHlyOjpmaWx0ZXIoZGYsIGlzLmluZmluaXRlKHBzZXVkb2J1bGtfbG9nX2NvdW50cykpCiAgKXw+CiAgcHVycnI6Omxpc3RfcmJpbmQobmFtZXNfdG8gPSAicHJvamVjdF9pZCIpCgoKZ2dwbG90KGRlc2VxX2luZl9kZikgKwogIGFlcyh4ID0gcHNldWRvYnVsa19kZXNlcSwgZmlsbCA9IHByb2plY3RfaWQpICsgCiAgZ2VvbV9oaXN0b2dyYW0oYmlucyA9IDE1KSArIAogIGZhY2V0X3dyYXAodmFycyhwcm9qZWN0X2lkKSwgbnJvdyA9IDEpICsgCiAgdGhlbWUobGVnZW5kLnBvc2l0aW9uID0gIm5vbmUiKQpgYGAKClNvLCBsb3RzIG9mIHRoZSAwcyBhcmUgaGVyZSB3aGljaCB3ZSBleHBlY3QgdG8gc2VlLgpBcmUgX2FsbF8gdGhlIDBzIGhlcmU/CgpJZiBhbGwgdGhlIDBzIGFyZSB0aGUgc2FtZSBhcyBhbGwgdGhlIGluZmluaXRlcywgdGhlbiB0aGVyZSBzaG91bGQgYmUgbm8gcm93cyB3aGVyZSAwIGFuZCBmaW5pdGUgY28tb2NjdXI6CgoKYGBge3J9CnByb2plY3Rfd2lkZV9kZl9saXN0IHw+CiAgcHVycnI6Om1hcCgKICAgIFwoZGYpIHsKICAgICAgZGYgfD4KICAgICAgICAjIHdoZW4gZGVzZXEgaXMgMCwgaXMgbG9nX2NvdW50cyBhbHdheXMgLWluZj8KICAgICAgICBkcGx5cjo6ZmlsdGVyKAogICAgICAgICAgcHNldWRvYnVsa19kZXNlcSA9PSAwLCAKICAgICAgICAgIGlzLmZpbml0ZShwc2V1ZG9idWxrX2xvZ19jb3VudHMpCiAgICAgICAgKSB8PgogICAgICAgIG5yb3coKQogIH0pCmBgYApJbmRlZWQsIGBwc2V1ZG9idWxrX2xvZ19jb3VudHNgIGlzIG5ldmVyIGZpbml0ZSAoYWthLCBpdCBpcyBhbHdheXMgYC1JbmZgKSB3aGVuIGBwc2V1ZG9idWxrX2Rlc2VxYCBpcyAwLCB3aGljaCBpcyBnb29kIHRvIGtub3cuCgoKTGV0J3MgYWxzbyBjaGVjayBpZiB3ZSBoYXZlIHRoZSBzYW1lIGdlbmVzIGJldHdlZW4gYnVsayBhbmQgc2luZ2xlLWNlbGwuCkFueSBnZW5lcyBwcmVzZW50IGluIG9uZSBtb2RhbGl0eSBidXQgbm90IHRoZSBvdGhlciB3aWxsIGhhdmUgYE5BYCBleHByZXNzaW9uICh0cmFuc2Zvcm1pbmcgYW55IDAgZXhwcmVzc2lvbnMgd2lsbCBoYXZlIHByb2R1Y2VkIGAtSW5mYCwgbm90IGBOQWAsIHNvIHRob3NlIGBOQWBzIGFyZSBmcm9tIGpvaW5pbmcpLgoKQXJlIGFueSBpbiBzaW5nbGUtY2VsbCBidXQgbm90IGJ1bGs/CmBgYHtyfQpwcm9qZWN0X3dpZGVfZGZfbGlzdCB8PgogIHB1cnJyOjptYXAoXChkZil7CiAgICBkZiB8PiAKICAgICAgZHBseXI6OmZpbHRlcighaXMubmEoYnVsa190cG0pLCAKICAgICAgICAgICAgICAgICAgICBpcy5uYShwc2V1ZG9idWxrX2Rlc2VxKSwgCiAgICAgICAgICAgICAgICAgICAgaXMubmEocHNldWRvYnVsa19sb2dfY291bnRzKSkgCiAgfSkgfD4KICBkcGx5cjo6YmluZF9yb3dzKCkKCmBgYApOb3BlISBIb3cgYWJvdXQgc2luZ2xlLWNlbGwgYnV0IG5vdCBidWxrPwpgYGB7cn0Kb25seV9zaW5nbGVfY2VsbCA8LSBwcm9qZWN0X3dpZGVfZGZfbGlzdCB8PgogIHB1cnJyOjptYXAoXChkZil7CiAgICBkZiB8PiAKICAgICAgZHBseXI6OmZpbHRlcihpcy5uYShidWxrX3RwbSksIAogICAgICAgICAgICAgICAgICAgICFpcy5uYShwc2V1ZG9idWxrX2Rlc2VxKSwgCiAgICAgICAgICAgICAgICAgICAgIWlzLm5hKHBzZXVkb2J1bGtfbG9nX2NvdW50cykpIAogIH0pIHw+CiAgcHVycnI6Omxpc3RfcmJpbmQobmFtZXNfdG8gPSAicHJvamVjdF9pZCIpIHw+CiAgZHBseXI6OmNvdW50KHByb2plY3RfaWQsIGVuc2VtYmxfaWQpCm9ubHlfc2luZ2xlX2NlbGwKYGBgCgoKWXVwIQpQbHVzLCB0aGVzZSBudW1iZXJzIGFyZSBhbGwgdGhlIHNhbXBsZSBudW1iZXJzLi4uCgpgYGB7cn0Kb25seV9zaW5nbGVfY2VsbCB8PgogIGRwbHlyOjpjb3VudChlbnNlbWJsX2lkKQpgYGAKLi5hbmQgaXQncyB0aGUgc2FtZSBnZW5lcyBhbGwgcm91bmQgKHByb2JhYmx5IGEgcmVmZXJlbmNlIHRoaW5nLi4uPykuCgoKSW4gYW55IGNhc2UsIHdoYXQgaXMgdGhlIGV4cHJlc3Npb24gb2YgdGhlc2UgZ2VuZXMgaW4gcHNldWRvYnVsaz8KYGBge3IgZmlnLmhlaWdodD0zLCBmaWcud2lkdGg9OH0KZ2VuZXNfbm90X2luX2J1bGsgPC0gcHJvamVjdF93aWRlX2RmX2xpc3QgfD4KICBwdXJycjo6bWFwKAogIFwoZGYpIGRmIHw+IGRwbHlyOjpmaWx0ZXIoaXMubmEoYnVsa190cG0pKSAKKSB8PgogIHB1cnJyOjpsaXN0X3JiaW5kKG5hbWVzX3RvID0gInByb2plY3RfaWQiKSB8PgogIGRwbHlyOjpzZWxlY3QoLWJ1bGtfdHBtKSB8PgogIHRpZHlyOjpwaXZvdF9sb25nZXIoCiAgICBjb250YWlucygicHNldWRvYnVsayIpLCAKICAgIG5hbWVzX3RvID0gImV4cHJlc3Npb25fdHlwZSIsIAogICAgdmFsdWVzX3RvID0gImV4cHJlc3Npb24iCiAgKQoKZ2dwbG90KGdlbmVzX25vdF9pbl9idWxrKSArIAogIGFlcyh4ID0gZXhwcmVzc2lvbl90eXBlLCB5ID0gZXhwcmVzc2lvbikgKwogIGdlb21fYm94cGxvdCgpICsgCiAgZmFjZXRfd3JhcCh2YXJzKHByb2plY3RfaWQpLCBucm93ID0gMSkgKwogIHRoZW1lKGF4aXMudGV4dC54ID0gZWxlbWVudF90ZXh0KGFuZ2xlID0gMzAsIGhqdXN0ID0gMSkpCmBgYApTb21lIG9mIHRoZXNlIGdlbmVzIGFjdHVhbGx5IGhhdmUgZGVjZW50IHZhbHVlcyBpbiBzaW5nbGUtY2VsbC4KCgojIyMgRnVsbCBkaXN0cmlidXRpb25zCgpIZXJlLCB3ZSB2aXN1YWxpemUgZGlzdHJpYnV0aW9ucyBvZiBhbGwgcXVhbnRpdGllczoKCmBgYHtyLCBmaWcud2lkdGggPSAxMiwgZmlnLmhlaWdodCA9IDgsIHdhcm5pbmcgPSBGQUxTRX0KcHJvamVjdF9sb25nX2RmX2xpc3QgfD4KICBwdXJycjo6aW1hcCgKICAgIFwoZGYsIHByb2plY3RfaWQpIHsKICAgICAgIGdncGxvdChkZikgKyAKICAgICAgICAgIGFlcyh4ID0gZXhwcmVzc2lvbiwgZmlsbCA9IGV4cHJlc3Npb25fdHlwZSkgKyAKICAgICAgICAgIGdlb21fZGVuc2l0eShhbHBoYSA9IDAuNSkgKyAKICAgICAgICAgIHNjYWxlX2ZpbGxfYnJld2VyKHBhbGV0dGUgPSAiRGFyazIiKSArIAogICAgICAgICAgZmFjZXRfd3JhcCh2YXJzKGV4cHJlc3Npb25fdHlwZSksIHNjYWxlcyA9ICJmcmVlIiwgbnJvdyA9IDQpICsKICAgICAgICAgIGdndGl0bGUocHJvamVjdF9pZCkgKwogICAgICAgICAgdGhlbWUobGVnZW5kLnBvc2l0aW9uID0gIm5vbmUiKQogICAgfQogICkgfD4KICBwYXRjaHdvcms6OndyYXBfcGxvdHMoZ3VpZGVzID0gImNvbGxlY3QiLCBucm93ID0gMSkKYGBgCgoKCiMjIFJlbGF0aW9uc2hpcCBiZXR3ZWVuIHF1YW50aXRpZXMKCgpUaGlzIHNlY3Rpb24gd2lsbCBsb29rIGF0IHRoZSByZWxhdGlvbnNoaXAgYmV0d2VlbiBidWxrIGFuZCBwc2V1ZG9idWxrIHF1YW50aXRpZXMgd2l0aDoKCiogc2NhdHRlcnBsb3RzIHRvIGNvbmZpcm0gaWYgd2UgaGF2ZSBhIGxpbmVhciByZWxhdGlvbnNoaXAKKiBwZXItc2FtcGxlIGxpbmVhciBtb2RlbHMgb2YgYGJ1bGsgfiBwc2V1ZG9idWxrYCB0byBkbyBhIGN1cnNvcnkgY29tcGFyaXNvbiBvZiBtb2RlbCBzdGF0aXN0aWNzCiAgKiB0aGVzZSByZXN1bHRzIGFyZSBwcmVzZW50ZWQgYm90aCBhcyBwbG90cyBhbmQgYXMgdGhlIGZ1bGwgcGVyLXNhbXBsZSB0YWJsZSB0byBzY3JvbGwgdGhyb3VnaAogIAogIAojIyMgU2NhdHRlcnBsb3RzIAoKV2hhdCBkb2VzIHRoZSByZWxhdGlvbnNoaXAgbG9vayBsaWtlIGJldHdlZW4gYnVsayBhbmQgZWFjaCBmbGF2b3Igb2YgcHNldWRvYnVsaz8KUGxvdHMgYXJlIG9yZ2FuaXplZCBieSBwcm9qZWN0IGFuZDoKCiogTGVmdC1zaWRlIHBhbmVscyBhcmUgYGJ1bGsgdHBtIH4gZGVzZXEgcHNldWRvY291bnRzYAoqIFJpZ2h0LXNpZGUgcGFuZWxzIGFyZSBgYnVsayB0cG0gfiBsb2dfY291bnRzIHBzZXVkb2NvdW50c2AKCmBgYHtyIGZpZy5oZWlnaHQ9MzUsIGZpZy53aWR0aD0xOCwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KcHJvamVjdF93aWRlX2RmX2xpc3QgfD4KICBwdXJycjo6aW1hcCgKICAgIFwoZGYsIHByb2plY3RfaWQpIHsKICAgICAgCiAgICAgIHAxIDwtIGdncGxvdChkZikgKyAKICAgICAgICBhZXMoeCA9IHBzZXVkb2J1bGtfZGVzZXEsIHkgPSBidWxrX3RwbSkgKyAKICAgICAgICBnZW9tX3BvaW50KGFscGhhID0gMC4yLCBzaXplID0gMC41KSArIAogICAgICAgIGdlb21fc21vb3RoKG1ldGhvZCA9ICJsbSIpICsgCiAgICAgICAgZmFjZXRfd3JhcCh2YXJzKHNhbXBsZV9pZCksIG5yb3cgPSA1KSArCiAgICAgICAgZ2d0aXRsZSgiYnVsayB0cG0gfiBkZXNlcSIpCiAgICAgIAogICAgICBwMiA8LSBnZ3Bsb3QoZGYpICsgCiAgICAgICAgYWVzKHggPSBwc2V1ZG9idWxrX2xvZ19jb3VudHMsIHkgPSBidWxrX3RwbSkgKyAKICAgICAgICBnZW9tX3BvaW50KGFscGhhID0gMC4yLCBzaXplID0gMC41KSArIAogICAgICAgIGdlb21fc21vb3RoKG1ldGhvZCA9ICJsbSIpICsKICAgICAgICBmYWNldF93cmFwKHZhcnMoc2FtcGxlX2lkKSwgbnJvdyA9IDUpICsKICAgICAgICBnZ3RpdGxlKCJidWxrIHRwbSB+IGxvZ2NvdW50cyIpIAogICAgICAgICAgIAogICAgICAKICAgICAgcGF0Y2h3b3JrOjp3cmFwX3Bsb3RzKHAxLCBwMikgCgogICAgfQogICkgfD4KICBwYXRjaHdvcms6OndyYXBfcGxvdHMobnJvdyA9IDUpCmBgYAoKCiMjIyBTdGF0aXN0aWNzIAoKTGV0J3Mgbm93IGdldCBzb21lIHN0YXRzIGZvciBlYWNoIHNhbXBsZS4KV2UnbGwgZml0IGEgbGluZWFyIG1vZGVsIGZvciBlYWNoIHNhbXBsZSwgYW5kIGRpc3BsYXkgc29tZSBxdWFudGl0aWVzIGJlbG93IGJvdGggYXMgYm94cGxvdHMgYW5kIHRoZSBmdWxsIHRhYmxlLgoKYGBge3IgZmlnLmhlaWdodCA9IDEwLCBmaWcud2lkdGggPSAxMn0KCnBsb3Rfc3RhdHMgPC0gZnVuY3Rpb24oZGYsIGNvbHVtbiwgdGl0bGUpIHsKICBnZ3Bsb3QoZGYpICsgCiAgICBhZXMoeCA9IGV4cHJlc3Npb25fdHlwZSwgeSA9IHt7Y29sdW1ufX0sIGNvbG9yID0gZXhwcmVzc2lvbl90eXBlKSArIAogICAgZ2VvbV9ib3hwbG90KCkgKyAKICAgIHNjYWxlX2NvbG9yX2JyZXdlcihwYWxldHRlID0gIkRhcmsyIikgKwogICAgZ2d0aXRsZSh0aXRsZSkgKwogICAgZmFjZXRfd3JhcCh2YXJzKHByb2plY3RfaWQpLCBucm93ID0gMSkgKwogICAgdGhlbWUobGVnZW5kLnBvc2l0aW9uID0gIm5vbmUiKQp9Cgptb2RlbF9zYW1wbGVzIDwtIGZ1bmN0aW9uKGlkLCBkZikgewogIHNhbXBsZV9kZiA8LSBkZiB8PgogICAgZHBseXI6OmZpbHRlcihzYW1wbGVfaWQgPT0gaWQpIAogIAogIGRmX2Rlc2VxIDwtIHNhbXBsZV9kZiB8PgogICAgZHBseXI6OmZpbHRlcihpcy5maW5pdGUocHNldWRvYnVsa19kZXNlcSksIGlzLmZpbml0ZShidWxrX3RwbSkpCiAgZml0X2Rlc2VxIDwtIGxtKGJ1bGtfdHBtIH4gcHNldWRvYnVsa19kZXNlcSwgZGF0YSA9IGRmX2Rlc2VxKSAKCiAgZGZfbG9nX2NvdW50cyA8LSBzYW1wbGVfZGYgfD4KICAgICAgZHBseXI6OmZpbHRlcihpcy5maW5pdGUocHNldWRvYnVsa19sb2dfY291bnRzKSwgaXMuZmluaXRlKGJ1bGtfdHBtKSkKICBmaXRfbG9nX2NvdW50cyA8LSBsbShidWxrX3RwbSB+IHBzZXVkb2J1bGtfbG9nX2NvdW50cywgZGF0YSA9IGRmX2xvZ19jb3VudHMpCiAgICAgIAogICMgVGFidWxhdGUgYW5kIHJldHVybiBzb21lIGZpdCBzdGF0cwogIGRhdGEuZnJhbWUoCiAgICBleHByZXNzaW9uX3R5cGUgPSBjKCJkZXNlcSIsICJsb2dfY291bnRzIiksCiAgICByc3F1YXJlZCA9IGMoYnJvb206OmdsYW5jZShmaXRfZGVzZXEpJHIuc3F1YXJlZCwgYnJvb206OmdsYW5jZShmaXRfbG9nX2NvdW50cykkci5zcXVhcmVkKSwgCiAgICBjb2VmZiA9IGMoYnJvb206OnRpZHkoZml0X2Rlc2VxKSRlc3RpbWF0ZVsyXSwgYnJvb206OnRpZHkoZml0X2xvZ19jb3VudHMpJGVzdGltYXRlWzJdKSwgCiAgICByZXNpZHVhbF9zZCA9IGMoYnJvb206OmdsYW5jZShmaXRfZGVzZXEpJHNpZ21hLCBicm9vbTo6Z2xhbmNlKGZpdF9sb2dfY291bnRzKSRzaWdtYSkKICApCn0KCnN0YXRzX2RmIDwtIHByb2plY3Rfd2lkZV9kZl9saXN0IHw+CiAgcHVycnI6Om1hcCgKICAgIFwoZGYpIHsKICAgICAgCiAgICAgICMgV2UgbmVlZCB0byBtYXAgb3ZlciBzYW1wbGUgaWRzIG5vdwogICAgICBzYW1wbGVzIDwtIHVuaXF1ZShkZiRzYW1wbGVfaWQpCiAgICAgIG5hbWVzKHNhbXBsZXMpIDwtIHNhbXBsZXMKICAgICAgCiAgICAgIGZpdF90YWJsZSA8LSBzYW1wbGVzIHw+CiAgICAgICAgcHVycnI6Om1hcChtb2RlbF9zYW1wbGVzLCBkZikgfD4KICAgICAgICBwdXJycjo6bGlzdF9yYmluZChuYW1lc190byA9ICJzYW1wbGVfaWQiKQogICAgICAKICAgICAgcmV0dXJuKGZpdF90YWJsZSkKCiAgICB9CiAgKSB8PgogICMgbm93LCBjb21iaW5lIGFsbCBwcm9qZWN0cyBpbnRvIGEgc2luZ2xlIHRhYmxlCiAgcHVycnI6Omxpc3RfcmJpbmQobmFtZXNfdG8gPSAicHJvamVjdF9pZCIpCgpwYXRjaHdvcms6OndyYXBfcGxvdHMoCiAgcGxvdF9zdGF0cyhzdGF0c19kZiwgcnNxdWFyZWQsICJyc3F1YXJlZCIpLAogIHBsb3Rfc3RhdHMoc3RhdHNfZGYsIGNvZWZmLCAiY29lZmYiKSwgCiAgcGxvdF9zdGF0cyhzdGF0c19kZiwgcmVzaWR1YWxfc2QsICJyZXNpZHVhbF9zZCIpLCAKICBucm93ID0gMwopIAoKCgpgYGAKCiogUHNldWRvYnVsayBxdWFudGl0aWVzIGFyZSBtb3N0bHkgc2ltaWxhciwgYW5kIGBwc2V1ZG9ib2xrX2Rlc2VxYCB0ZW5kcyB0byBvdXRwZXJmb3JtIGBwc2V1ZG9ib2xrX2xvZ19jb3VudHNgIGZvciBhbGwgcHJvamVjdHMgZXhjZXB0IGBTQ1BDUDAwMDAwOWAsIGJ1dCBub3RhYmx5IHRoZXJlIGFyZSBvbmx5IDMgc2FtcGxlcyBmb3IgdGhpcyBwcm9qZWN0CiogQ29ycmVsYXRpb25zIGFyZSBtb3N0bHkgemVybyBvciB2ZXJ5IGxvdyBmb3IgYFNDUENQMDAwMDE3YCwgYW5kIGBTQ1BDUDAwMDAwMWAgYW5kIGBTQ1BDUDAwMDAwMmAgc2hvdyB0aGUgc3Ryb25nZXN0IHJlbGF0aW9uc2hpcHMgb3ZlcmFsbAoKCkFsbCB0aGUgYWN0dWFsIHZhbHVlcyBhcmUgaGVyZToKCgpgYGB7cn0Kc3RhdHNfZGYKYGBgCgoKCgojIyBTZXNzaW9uIGluZm8KCmBgYHtyfQpzZXNzaW9uSW5mbygpCmBgYA==